lib/repo: Add min-free-space-percent option, default 3%
authorColin Walters <walters@verbum.org>
Thu, 29 Jun 2017 20:51:56 +0000 (16:51 -0400)
committerAtomic Bot <atomic-devel@projectatomic.io>
Tue, 4 Jul 2017 16:15:11 +0000 (16:15 +0000)
For ostree-as-host, we're the superuser, so we'll blow past
any reserved free space by default.  While deltas have size
metadata, if one happens to do a loose fetch, we can fill
up the disk.

Another case is flatpak: the system helper has similar concerns
here as ostree-as-host, and for `flatpak --user`, we also
want to be nice and avoid filling up the user's quota.

Closes: https://github.com/ostreedev/ostree/issues/962
Closes: #987
Approved by: jlebon

man/ostree.repo-config.xml
src/libostree/ostree-repo-commit.c
src/libostree/ostree-repo-private.h
src/libostree/ostree-repo.c
tests/installed/itest-pull-space.sh [new file with mode: 0755]

index d187f89fdcec8393a60949824fb9f0af78798859..60458dfaaca4639962b40d7de6498e4dd7bd7519 100644 (file)
@@ -190,6 +190,14 @@ Boston, MA 02111-1307, USA.
         <term><varname>unconfigured-state</varname></term>
         <listitem><para>If set, pulls from this remote will fail with the configured text.  This is intended for OS vendors which have a subscription process to access content.</para></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><varname>min-free-space-percent</varname></term>
+        <listitem><para>Integer percentage value (0-99) that specifies a minimum
+        percentage of total space (in blocks) in the underlying filesystem to
+        keep free. The default value is 3.</para></listitem>
+      </varlistentry>
+
     </variablelist>
 
   </refsect1>
index 9cc028e0308dc3149177095354bc118139a5347c..185ab82f62be13dd23b28916c52e7ece3262bece 100644 (file)
@@ -23,6 +23,7 @@
 #include "config.h"
 
 #include <glib-unix.h>
+#include <sys/statvfs.h>
 #include <gio/gfiledescriptorbased.h>
 #include <gio/gunixinputstream.h>
 #include <gio/gunixoutputstream.h>
@@ -574,6 +575,24 @@ write_content_object (OstreeRepo         *self,
   else
     size = 0;
 
+  /* Free space check; only applies during transactions */
+  if (self->min_free_space_percent > 0 && self->in_transaction)
+    {
+      g_mutex_lock (&self->txn_stats_lock);
+      g_assert_cmpint (self->txn_blocksize, >, 0);
+      const fsblkcnt_t object_blocks = (size / self->txn_blocksize) + 1;
+      if (object_blocks > self->max_txn_blocks)
+        {
+          g_mutex_unlock (&self->txn_stats_lock);
+          g_autofree char *formatted_required = g_format_size ((guint64)object_blocks * self->txn_blocksize);
+          return glnx_throw (error, "min-free-space-percent '%u%%' would be exceeded, %s more required",
+                             self->min_free_space_percent, formatted_required);
+        }
+      /* This is the main bit that needs mutex protection */
+      self->max_txn_blocks -= object_blocks;
+      g_mutex_unlock (&self->txn_stats_lock);
+    }
+
   /* For regular files, we create them with default mode, and only
    * later apply any xattrs and setuid bits.  The rationale here
    * is that an attacker on the network with the ability to MITM
@@ -1080,6 +1099,29 @@ ostree_repo_prepare_transaction (OstreeRepo     *self,
   memset (&self->txn_stats, 0, sizeof (OstreeRepoTransactionStats));
 
   self->in_transaction = TRUE;
+  if (self->min_free_space_percent > 0)
+    {
+      struct statvfs stvfsbuf;
+      if (TEMP_FAILURE_RETRY (fstatvfs (self->repo_dir_fd, &stvfsbuf)) < 0)
+        return glnx_throw_errno_prefix (error, "fstatvfs");
+      g_mutex_lock (&self->txn_stats_lock);
+      self->txn_blocksize = stvfsbuf.f_bsize;
+      /* Convert fragment to blocks to compute the total */
+      guint64 total_blocks = (stvfsbuf.f_frsize * stvfsbuf.f_blocks) / stvfsbuf.f_bsize;
+      /* Use the appropriate free block count if we're unprivileged */
+      guint64 bfree = (getuid () != 0 ? stvfsbuf.f_bavail : stvfsbuf.f_bfree);
+      guint64 reserved_blocks = ((double)total_blocks) * (self->min_free_space_percent/100.0);
+      if (bfree > reserved_blocks)
+        self->max_txn_blocks = bfree - reserved_blocks;
+      else
+        {
+          g_mutex_unlock (&self->txn_stats_lock);
+          g_autofree char *formatted_free = g_format_size (bfree * self->txn_blocksize);
+          return glnx_throw (error, "min-free-space-percent '%u%%' would be exceeded, %s available",
+                             self->min_free_space_percent, formatted_free);
+        }
+      g_mutex_unlock (&self->txn_stats_lock);
+    }
 
   gboolean ret_transaction_resume = FALSE;
   if (!_ostree_repo_allocate_tmpdir (self->tmp_dir_fd,
index d518e52b10d335d10db3e304309a21817936f905..9e00cf40df4caafb550905cf25c5cb67f041e07d 100644 (file)
@@ -20,6 +20,7 @@
 
 #pragma once
 
+#include <sys/statvfs.h>
 #include "ostree-ref.h"
 #include "ostree-repo.h"
 #include "ostree-remote-private.h"
@@ -102,6 +103,9 @@ struct OstreeRepo {
   GHashTable *txn_collection_refs;  /* (element-type OstreeCollectionRef utf8) */
   GMutex txn_stats_lock;
   OstreeRepoTransactionStats txn_stats;
+  /* Implementation of min-free-space-percent */
+  gulong txn_blocksize;
+  fsblkcnt_t max_txn_blocks;
 
   GMutex cache_lock;
   guint dirmeta_cache_refcount;
@@ -123,6 +127,7 @@ struct OstreeRepo {
   uid_t owner_uid;
   uid_t target_owner_uid;
   gid_t target_owner_gid;
+  guint min_free_space_percent;
 
   guint test_error_flags; /* OstreeRepoTestErrorFlags */
 
index a02214e102c5152dd2a17a685f06eded17f7cbc8..e29b8fca8e2e365b54fe937e28716048f7f2f54d 100644 (file)
@@ -44,6 +44,7 @@
 #include <locale.h>
 #include <glib/gstdio.h>
 #include <sys/file.h>
+#include <sys/statvfs.h>
 
 /**
  * SECTION:ostree-repo
@@ -1964,6 +1965,20 @@ reload_core_config (OstreeRepo          *self,
       self->zlib_compression_level = OSTREE_ARCHIVE_DEFAULT_COMPRESSION_LEVEL;
   }
 
+  { g_autofree char *min_free_space_percent_str = NULL;
+    /* If changing this, be sure to change the man page too */
+    const char *default_min_free_space = "3";
+
+    if (!ot_keyfile_get_value_with_default (self->config, "core", "min-free-space-percent",
+                                            default_min_free_space,
+                                            &min_free_space_percent_str, error))
+      return FALSE;
+
+    self->min_free_space_percent = g_ascii_strtoull (min_free_space_percent_str, NULL, 10);
+    if (self->min_free_space_percent > 99)
+      return glnx_throw (error, "Invalid min-free-space-percent '%s'", min_free_space_percent_str);
+  }
+
   {
     g_clear_pointer (&self->collection_id, g_free);
     if (!ot_keyfile_get_value_with_default (self->config, "core", "collection-id",
diff --git a/tests/installed/itest-pull-space.sh b/tests/installed/itest-pull-space.sh
new file mode 100755 (executable)
index 0000000..36703a4
--- /dev/null
@@ -0,0 +1,24 @@
+#!/bin/bash
+# Test min-free-space-percent using loopback devices
+
+set -xeuo pipefail
+
+dn=$(dirname $0)
+. ${dn}/libinsttest.sh
+
+test_tmpdir=$(prepare_tmpdir)
+trap _tmpdir_cleanup EXIT
+
+cd ${test_tmpdir}
+truncate -s 100MB testblk.img
+blkdev=$(losetup --find --show $(pwd)/testblk.img)
+mkfs.xfs ${blkdev}
+mkdir mnt
+mount ${blkdev} mnt
+ostree --repo=mnt/repo init --mode=bare-user
+if ostree --repo=mnt/repo pull-local /ostree/repo ${host_commit} 2>err.txt; then
+    fatal "succeeded in doing a pull with no free space"
+fi
+assert_file_has_content err.txt "min-free-space-percent"
+umount mnt
+losetup -d ${blkdev}